<?php

class Repository //implements IRepository
{

  /**
   *
   * @var array
   */
  protected static $instances = array();

  /**
   *
   * @var IDomObjDefinition
   */
  protected $def;

  /**
   *
   * @param string $name
   * @return Repository
   */
  public static function getInstance($name)
  {
    if (empty(self::$instances[$name])) {
      self::$instances[$name] = new self($name);
    }

    return self::$instances[$name];
  }

  /**
   *
   * @param string $name
   * @return void
   */
  protected function __construct($name)
  {
    $this->def = Domain::getInstance()->getObject($name);
  }

  /**
   *
   * @param $name
   * @param $data
   * @return unknown_type
   */
  public function delete($data)
  {
    $def = $this->def;

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        if (isset($data[$assoc_name])) {
          $def->getAssociation($assoc_name)->getDAO()->beforeDelete($data);
        }
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }

    $def = Domain::toEntity($def);

    $table = $def->getTable();
    $pk = $def->getPk();

    $id = $data[$pk];

    $dao = DAO::getInstance();

    $query = $dao->query("DELETE FROM $table WHERE $pk = ?");
    $result = $query->execute($id);

    $tables = $def->getTables();
    if (! empty($tables)) {
      foreach ($tables as $_table) {
        if ($_table != $table) {
          $query = $dao->query("DELETE FROM $_table WHERE $pk = ?");
          $query->execute($id);
        }
      }
    }

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        if (isset($data[$assoc_name])) {
          $def->getAssociation($assoc_name)->getDAO()->afterDelete($data);
        }
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
  }

  /**
   *
   * @param IDomObjDefinition $def
   * @param array $data
   * @return void
   */
  public function save(&$data)
  {
    $def = $this->def;

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        $def->getAssociation($assoc_name)->getDAO()->beforeSave($data);
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
    catch (AssociationNotFoundException $e) {
      //
    }

    $def = Domain::toEntity($def);
    if (empty($data[$def->getPk()])) {
      $this->insert($data);
    }
    else {
      $this->update($data);
    }

    try {
      $def = Domain::toAggregate($def);
      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        $def->getAssociation($assoc_name)->getDAO()->afterSave($data);
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
    catch (AssociationNotFoundException $e) {
      //
    }
  }

  /**
   *
   * @param array $data
   * @return array
   */
  public function insert(&$data)
  {
    $def = Domain::toEntity($this->def);

    $table = $def->getTable();
    $pk = $def->getPk();

    $_data = self::collectData($def, $data, $table);

    $dao = DAO::getInstance();

    $sql = "INSERT INTO $table " . QueryObject::buildInsert($_data);
    $query = $dao->query($sql);

    $result = $query->execute($_data);

    $data[$pk] = $dao->lastInsertId();

    $this->saveRelated($data);
  }

  /**
   *
   * @param array $data
   * @return void
   */
  public function update(&$data)
  {
    $def = Domain::toEntity($this->def);

    $table = $def->getTable();
    $pk = $def->getPk();

    $_data = self::collectData($def, $data, $table);

    $id = $_data[$pk];

    unset($_data[$pk]);

    if (! empty($_data)) {
      $dao = DAO::getInstance();

      $query = $dao->query("UPDATE $table SET " . QueryObject::buildUpdate($_data) . " WHERE $pk = :$pk");

      $_data[$pk] = $id;

      $result = $query->execute($_data);

      $this->saveRelated($data);
    }
  }

  protected function saveRelated(&$data)
  {
    $def = Domain::toEntity($this->def);

    $table = $def->getTable();
    $pk = $def->getPk();

    $id = $data[$pk];

    $tables = $def->getTables();
    if (! empty($tables)) {
      foreach ($tables as $_table) {
        if ($_table != $table) {
          $_data = self::collectData($def, $data, $_table);
          $_data[$pk] = $id;

          $dao = DAO::getInstance();

          $query = $dao->query("INSERT INTO $_table " . QueryObject::buildInsert($_data)
            . " ON DUPLICATE KEY UPDATE " . QueryObject::buildUpdate($_data));

          $result = $query->execute($_data);
        }
      }
    }
  }

  public function find($params = array())
  {
    $params = array_merge(array('depth' => 0, 'mappingDepth' => 0), ( array ) $params);

    $def = Domain::toEntity($this->def);

    $table = $def->getTable();

    $query = DAO::getInstance()->query()->from($table);

    $tables = $def->getTables();
    if (! empty($tables)) {
      foreach ($tables as $_table) {
        if ($_table != $table) {
          $query->join($_table . ' ON ' . $table . '.' . $def->getPk() . ' = ' . $_table . '.' . $def->getPk(), QueryObject::JOIN_LEFT);
        }
      }
    }

    $this->setFields($query, $params);
    $this->setOrderBy($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $query->limit(1);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchRow();

    if (! $result->numRows()) {
      return $data;
    }

    $depth = sy_get_param($params, 'depth', 0);

    if ($depth != 0) {
      $this->intr($def, $data, $depth - 1);
    }

    $mappingDepth = $params['mappingDepth'];

    if ($mappingDepth == 0) {
      return $data;
    }

    $objs = AbstractFactory::factory($this->def, $data, $mappingDepth);

    return $objs;
  }

  public function findAll($params = array())
  {
    $params = array_merge(array('depth' => 0, 'mappingDepth' => 0), ( array ) $params);

    $def = Domain::toEntity($this->def);

    $table = $def->getTable();

    $query = DAO::getInstance()->query()->from($table);

    $tables = $def->getTables();
    if (! empty($tables)) {
      foreach ($tables as $_table) {
        if ($_table != $table) {
          $query->join($_table . ' ON ' . $table . '.' . $def->getPk() . ' = ' . $_table . '.' . $def->getPk(), QueryObject::JOIN_LEFT);
        }
      }
    }

    $this->setFields($query, $params);
    $this->setOrderBy($query, $params);
    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchAll();

    if (! $result->numRows()) {
      return $data;
    }

    $depth = sy_get_param($params, 'depth', 0);

    if ($depth != 0) {
      $this->intr($def, $data, $depth - 1);
    }

    $mappingDepth = $params['mappingDepth'];

    if ($mappingDepth == 0) {
      return $data;
    }

    return AbstractFactory::factoryCollection($this->def, $data, $mappingDepth);
  }

  public function findCount($params = array())
  {
    $def = Domain::toEntity($this->def);

    $table = $def->getTable();

    $pk = $def->getPk();

    $query = DAO::getInstance()->query()->from($table)->select("COUNT($pk)");

    $tables = $def->getTables();
    if (! empty($tables)) {
      foreach ($tables as $_table) {
        if ($_table != $table) {
          $query->join($_table . ' ON ' . $table . '.' . $def->getPk() . ' = ' . $_table . '.' . $def->getPk(), QueryObject::JOIN_LEFT);
        }
      }
    }

    $this->setOrderBy($query, $params);
    $this->setLimit($query, $params);
    $this->setOffset($query, $params);
    $this->setWhere($query, $params);

    $result = $query->execute(sy_get_param($params, 'data'));

    $data = $result->fetchOne();

    return $data;
  }

  protected function intr($def, &$data, $depth = 0)
  {
    if ($depth <= - 255) {
      throw new Exception("Maximum recursions reached");
    }

    if (empty($data)) {
      return;
    }

    try {
      $def = Domain::toAggregate($def);

      foreach ($def->getAssociations() as $assoc_name => $assoc) {
        $assoc = $def->getAssociation($assoc_name);

        $assoc->getDAO()->find($data);

        $k = $def->getPk();

        if ($depth != 0) {
          if (isset($data[$k])) {
            $this->intr($assoc->getTarget(), $data[$assoc_name], $depth - 1);
          }
          else {
            $m = array();

            foreach ($data as &$row) {
              if (empty($row[$assoc_name])) {
                continue;
              }
              elseif (isset($row[$assoc_name][$k])) {
                $m[] = &$row;
              }
              elseif (isset($row[$assoc_name][$assoc->getForeignKey()])) {
                $m[] = &$row[$assoc_name];
              }
              else {
                foreach ($row[$assoc_name] as &$child) {
                  $m[] = &$child;
                }
              }
            }

            $this->intr($assoc->getTarget(), $m, $depth - 1);
          }
        }
      }
    }
    catch (DomainTypeConversionException $e) {
      //
    }
  }

  protected function setFields(IQueryObject $query, array $params)
  {
    if (isset($params['fields'])) {
      $query->select($params['fields']);
    }
    else {
      $query->select(self::collectFields($this->def));
    }
  }

  protected function setOrderBy(IQueryObject $query, array $params)
  {
    if (isset($params['orderBy'])) {
      $query->orderBy($params['orderBy']);
    }
  }

  protected function setLimit(IQueryObject $query, array $params)
  {
    if (isset($params['limit'])) {
      $query->limit($params['limit']);
    }
  }

  protected function setOffset(IQueryObject $query, array $params)
  {
    if (isset($params['offset'])) {
      $query->offset($params['offset']);
    }
  }

  protected function setWhere(IQueryObject $query, array $params)
  {
    if (isset($params['where'])) {
      if (is_array($params['where'])) {
        foreach ($params['where'] as $where) {
          $query->where($where);
        }
      }
      else {
        $query->where($params['where']);
      }
    }
  }

  public static function collectFields(IEntityDefinition $def, $addTable = true, $table = null)
  {
    $fields = array();

    foreach ($def->getFields() as $name => $field) {
      $field = $def->getField($name);

      $_table = $field->getTable();
      if (empty($_table)) {
        $_table = $def->getTable();
      }

      if (! empty($table) && $table != $_table) {
        continue;
      }

      $fieldName = $field->getName();
      if (empty($fieldName)) {
        $fieldName = $name;
      }

      $fields[] = ($addTable ? $_table . '.' : '') . $fieldName;
    }

    return $fields;
  }

  public static function collectData(IEntityDefinition $def, $data, $table = null)
  {
    if (empty($table)) {
      $table = $def->getTable();
    }

    $fields = self::collectFields($def, false, $table);
    $fields = array_flip($fields);

    if ($data instanceof IDictionary) {
      $data = $data->getAll();
    }

    return array_intersect_key($data, $fields);
  }

}

?>